package com.ke.rule.base;

import com.google.common.base.Stopwatch;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.drools.core.RuleBaseConfiguration;
import org.drools.decisiontable.InputType;
import org.drools.decisiontable.SpreadsheetCompiler;
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.conf.SequentialOption;
import org.kie.api.definition.KiePackage;
import org.kie.api.definition.rule.Rule;
import org.kie.api.io.Resource;
import org.kie.api.io.ResourceType;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.StatelessKieSession;
import org.kie.internal.builder.DecisionTableConfiguration;
import org.kie.internal.builder.DecisionTableInputType;
import org.kie.internal.builder.KnowledgeBuilderFactory;
import org.kie.internal.conf.SequentialAgendaOption;
import org.kie.internal.io.ResourceFactory;
import org.kie.internal.utils.KieHelper;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 决策表工具类
 * 
 * @author guyu
 *
 */
public final class DecisionTables {
  private static final Logger logger = LoggerFactory.getLogger(DecisionTables.class);

  //决策表文件所在目录
  static final String DECISION_TABLE_DIR = "/rules";
 
  // 规则文件的规则库缓存
  private static final ConcurrentMap<String, KieBase> kbases = new ConcurrentHashMap<>();
  
  // 决策表文件与文件中所包含阶段集合的映射关系，无相应阶段时则无需执行决策表，优化部分性能
  private static final ConcurrentMap<String, Set<MethodStepType>> dt2Steps = new ConcurrentHashMap<>();
  
  // 决策表文件与文件中所包含属性的映射关系
  private static final ConcurrentMap<String, Map<String, String>> dt2Attrs = new ConcurrentHashMap<>();
  
  // 根据决策表文件获取KieBase时的重入锁
  private static final ReentrantLock lock = new ReentrantLock();

  private final static KieContainer classpathKieContainer;
  private final static DecisionTableConfiguration dtableConf;
  private final static RuleBaseConfiguration kieBaseConf;
  static {
    // dump rules also play a warm-up
    Stopwatch watch = Stopwatch.createStarted();

    // drools
    kieBaseConf = new RuleBaseConfiguration();
    kieBaseConf.setSequential(true);
    kieBaseConf.setSequentialAgenda(RuleBaseConfiguration.SequentialAgenda.SEQUENTIAL);
    
    KieServices ks = KieServices.Factory.get();
    classpathKieContainer = ks.getKieClasspathContainer();
    
    StringBuffer dumpedRules = new StringBuffer("### kie-packages");
    classpathKieContainer.getKieBaseNames().forEach(name -> {
      dumpedRules.append(System.lineSeparator());
      dumpedRules.append(name);
      classpathKieContainer.getKieBase(name).getKiePackages().stream()
          .filter(pkg -> !pkg.getRules().isEmpty()).forEach(pkg -> {
            dumpedRules.append(System.lineSeparator());
            dumpedRules.append(String.format("> %s (%d)", pkg.getName(), pkg.getRules().size()));
          });
    });
    
    dtableConf = KnowledgeBuilderFactory.newDecisionTableConfiguration();
    dtableConf.setInputType(DecisionTableInputType.CSV);

    // decision tables
    dumpedRules.append(System.lineSeparator());
    dumpedRules.append("### decision-tables");
    Reflections reflections = new Reflections(new ConfigurationBuilder()
        .setUrls(ClasspathHelper.forPackage("rules"))
        .setScanners(Scanners.Resources));
    Set<String> csvFiles = reflections.getResources(Pattern.compile(".*\\.csv"));
    csvFiles.stream().sorted().map(file -> {
      return (file.startsWith("/") ? "" : "/") + file;
    }).map(file -> {
      // 将形如：/BOOT-INF/classes/rules/.... 转为 /rules/...
      return String.format("%s%s", DECISION_TABLE_DIR,
          StringUtils.substringAfter(file, DECISION_TABLE_DIR));
    }).forEach(dtableFile -> {
      Pair<Map<String, String>, Set<MethodStepType>> pair = parseDecisionTable(dtableFile);
      dt2Attrs.putIfAbsent(dtableFile, pair.getLeft());
      dt2Steps.putIfAbsent(dtableFile, pair.getRight());
      
      dumpedRules.append(System.lineSeparator());
      dumpedRules.append(String.format("> %s %s %s",
          StringUtils.substringAfter(dtableFile, DECISION_TABLE_DIR + "/"), pair.getLeft(), pair.getRight()));
    });
    
    logger.info("DecisionTables took {} ms to warm up\n{}", watch.elapsed(TimeUnit.MILLISECONDS), dumpedRules);
  }
  
  /**
   * 初始化，用于static代码块的预热加载
   */
  public static void initialize() {
    logger.info("DecisionTables initialize...");
  }
  
  /**
   * 加载决策表文件，获取KieSession
   * 
   * @param kieBaseName 知识库名称
   * @param dtableFile 决策表文件路径(xlsx or csv)
   * @return KieSession
   */
  public static StatelessKieSession getKieSessionByFile(String kieBaseName, String dtableFile) {
    Objects.requireNonNull(kieBaseName);
    Objects.requireNonNull(dtableFile);
    
    KieBase kieBase = kbases.get(dtableFile);
    if (null == kieBase) {
      Stopwatch watch = Stopwatch.createStarted();
      lock.lock();
      try {
        kieBase = kbases.get(dtableFile);
        if (null == kieBase) {
          // 优先从类路径中获取预编译后的规则库（仅当非开发测试环境）
          // 开发环境中修改了决策表若不编译则不生效，此时仍会使用老的预编译后的决策表，
          // 这为开发调试带来不便，尤其是在不清楚预编译机制的情况下，会在让决策表如何生效上花费时间
          // 故在开发阶段不走预编译
          boolean isDevTest = true;
          if (!isDevTest) {
            kieBase = getPrecompiledKieBase(kieBaseName, dtableFile);
          }
          
          // 预编译的规则库未找到则动态编译
          if (null == kieBase) {
            Resource dis = ResourceFactory.newClassPathResource(dtableFile, DecisionTables.class);
            dis.setConfiguration(dtableConf);

            if (logger.isDebugEnabled() || isDevTest) {
              SpreadsheetCompiler compiler = new SpreadsheetCompiler();
              InputType inputType = FilenameUtils.getExtension(dtableFile).equalsIgnoreCase("csv") ? InputType.CSV : InputType.XLS;
              logger.debug(compiler.compile(dis, inputType));
            }
            
            KieHelper helper = new KieHelper();
            helper.addResource(dis, ResourceType.DTABLE);

            kieBase = helper.build(SequentialOption.YES, SequentialAgendaOption.SEQUENTIAL);
          }
          kbases.putIfAbsent(dtableFile, kieBase);
        }
      } finally {
        lock.unlock();
      }
      if (logger.isDebugEnabled()) {
        logger.debug("getKieBase(kieBaseName: {}, dtableFile: {}) elapsed: {} ms", kieBaseName, dtableFile, watch.elapsed(TimeUnit.MILLISECONDS));
      }
    }
    return kieBase.newStatelessKieSession();
  }

  /**
   * 根据决策表文件查找预编译的规则，并构建出KieBase
   * 如果不存在预编译的Rule则返回null
   * 
   * @param kieBaseName 知识库名称
   * @param dtableFile /rules/fake/UpsertValidatorMatchingRules.csv
   * @return
   */
  private static KieBase getPrecompiledKieBase(String kieBaseName, String dtableFile) {
    Stopwatch watch = Stopwatch.createStarted();
        
    KieBase kieBase = null;
    try {
      kieBase = classpathKieContainer.newKieBase(kieBaseName, kieBaseConf);
    } catch (Exception e) {
      logger.error("classpathKieContainer.newKieBase error: {}", e.getMessage());
    }
    if (null == kieBase) {
      return null;
    }
    if (logger.isDebugEnabled()) {
      logger.debug("newKieBase({}) from classpathKieContainer elapsed: {} ms", kieBaseName, watch.elapsed(TimeUnit.MILLISECONDS));
    }
    
    int lastIndex = dtableFile.lastIndexOf(File.separator);
    
    // 决策表文件所在包名（路径）
    String packageName = "";
    if (lastIndex > 0) {
      packageName = dtableFile.substring(1, lastIndex);
      packageName = packageName.replaceAll(Matcher.quoteReplacement(File.separator), "\\.");
    }
    
    KiePackage kiePackage = kieBase.getKiePackage(packageName);
    if (null == kiePackage || kiePackage.getRules().isEmpty()) {
      return null;
    }
    
    // remove package
    List<String> kiePkgNames = kieBase.getKiePackages().stream().map(KiePackage::getName).collect(Collectors.toList());
    for (String name : kiePkgNames) {
      if (!name.equals(packageName)) {
        kieBase.removeKiePackage(name);
      }
    }
    
    // 决策表文件名称
    String tableName;
    if (lastIndex >= 0) {
      tableName = StringUtils.substringBefore(dtableFile.substring(lastIndex + 1), ".");
    } else {
      tableName = StringUtils.substringBefore(dtableFile, ".");
    }    
    List<String> exclRuleNames = Lists.newArrayList();
    for (Rule rule : kiePackage.getRules()) {
      if (rule.getName().startsWith(tableName + "_")) {
        continue;
      }
      exclRuleNames.add(rule.getName());
    }

    // remove rule
    for (String ruleName : exclRuleNames) {
      kieBase.removeRule(packageName, ruleName);
    }
    
    if (logger.isDebugEnabled()) {
      logger.debug("getPrecompiledKieBase: {} rules in {}.{}.{}, elapsed: {} ms", kiePackage.getRules().size(),
          kieBaseName, packageName, tableName, watch.elapsed(TimeUnit.MILLISECONDS));
    }
    return kieBase;
  }
  
  /**
   * 执行规则
   * 
   * @param kieSession
   * @param regularFact 固化的标准事实
   * @param moreFacts
   * @return firedNbr
   */
  private static void execute(StatelessKieSession kieSession, MatchingRulesFact regularFact, Object... moreFacts) {
    // 将MatchingRuleFact用作全局变量
    kieSession.setGlobal("links", regularFact);
    
    List<Object> elements = Lists.newArrayList();
    elements.add(regularFact);
    for (Object fact : moreFacts) {
      if (null == fact) {
        continue;
      }
      
      elements.add(fact);
    }
    kieSession.execute(elements);
  }

  /**
   * 获取决策表文件中的所有步骤
   * 
   * @param dtableFile
   * @return
   */
  public static Pair<Map<String, String>, Set<MethodStepType>> parseDecisionTable(String dtableFile) {
    Stopwatch stopwatch = Stopwatch.createStarted();
    
    Pair<Map<String, String>, List<AssyFuncUnit>> pair = DecisionTableParser.parseFile(dtableFile);
    
    // 删除不必要的属性，并将AssyFuncUnit转换为步骤名
    Stream.of("Import", "Variables", "Functions", "Sequential", "No-loop").forEach(k -> pair.getLeft().remove(k));
    Set<MethodStepType> steps = pair.getRight().stream().map(u -> MethodStepType.valueOf(u.getStep())).collect(Collectors.toSet());
    
    if (logger.isDebugEnabled()) {
      logger.debug("parseDecisionTable(dtableFile: {}) return [{} {}], elapsed: {} ms",
          dtableFile, pair.getLeft(), steps, stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }
    
    return Pair.of(pair.getLeft(), steps);
  }
  
  /**
   * 是否继承上级决策表的关键字
   */
  private static final String _SUCCESSION = "Succession";
  /**
   * 是否需要继承上级决策表
   * @param dtableFile
   * @return
   */
  public static boolean isInheritFromParent(String dtableFile) {
    Map<String, String> attrs = dt2Attrs.get(dtableFile);
    
    return null == attrs
        || !attrs.containsKey(_SUCCESSION)
        || Boolean.valueOf(attrs.get(_SUCCESSION));
  }
  
  /**
   * 指定步骤是否存在于决策表中（步骤中设置了需要执行的功能部件）
   * @param dtableFile
   * @param step
   * @return
   */
  public static boolean isStepExists(String dtableFile, MethodStepType step) {
    // 获取决策表文件中所包含的步骤
    Set<MethodStepType> steps = dt2Steps.get(dtableFile);
    return null == steps
        || steps.isEmpty()
        || steps.contains(step);
  }
  
  /**
   * 执行决策表
   * 
   * @param kieBaseName 知识库名称
   * @param dtableFile 决策表文件路径(xlsx or csv)
   * @param regularFact 固化的标准事实
   * @param moreParams 标准事实之外的动态事实
   * @return decision made or not, true:执行了决策表, false:未执行决策表
   */
  public static boolean execute(String kieBaseName, String dtableFile, MatchingRulesFact regularFact, Object... moreParams) {
    Stopwatch stopwatch = Stopwatch.createStarted();
    
    // 步骤集合中如果不包含决策事实中的步骤，则不必执行决策表，以便提升性能
    boolean decisionMade = isStepExists(dtableFile, regularFact.getStepType());
    if (decisionMade) {
      StatelessKieSession ksession = getKieSessionByFile(kieBaseName, dtableFile);
      if (logger.isDebugEnabled()) {
        logger.debug("getKieSessionByFile(kieBaseName: {}, dtableFile: {}) elapsed: {} ms", kieBaseName, dtableFile,
            stopwatch.elapsed(TimeUnit.MILLISECONDS));
      }
      execute(ksession, regularFact, moreParams);
    }
    
    if (logger.isDebugEnabled()) {
      String thinFactParams = Arrays.asList(moreParams).stream()
          .map(x -> String.format("%s(id:  )", x.getClass().getSimpleName()))
          .collect(Collectors.joining(", "));
      
      logger.debug("execute(kieBaseName: {}, dtableFile: {}, step: {}, factParams: {}), decisionMade: {}, duration: {} ms",
          kieBaseName, dtableFile, regularFact.getStepType(), thinFactParams, decisionMade,
          stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }
    
    return decisionMade;
  }
}
